package com.polarrose.wsf.client.impl;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.SocketTimeoutException;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.commons.httpclient.HostConfiguration;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.util.IdleConnectionTimeoutThread;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.polarrose.wsf.client.HttpProtocolException;
import com.polarrose.wsf.client.Invocation;
import com.polarrose.wsf.client.RequestInvoker;
import com.polarrose.wsf.client.ResultTransformationException;
import com.polarrose.wsf.client.WsfClientException;

public class HttpClientRequestInvoker implements RequestInvoker {

	public static final String DEFAULT_ENCODING = "UTF-8";

	private Logger logger = LoggerFactory.getLogger(HttpClientRequestInvoker.class);

	public static final String PARAM_VERSION = "Version";
	public static final String PARAM_ACTION = "Action";

	private HttpClient httpClient;
	private int connectionTimeout = 4000;
	private int defaultMaxConnectionsPerHost = 250;
	private int maxConnectionsPerHost = 250;
	private int maxTotalConnections = 2000;
	private int soTimeout = 15000;
	private int idleConnectionTimeout = 5000;
	private int idleConnectionTimeoutInterval = 5000;

	private synchronized void initializeDefaultHttpClient() {
		if (httpClient != null) {
			return;
		}
		MultiThreadedHttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager();
		connectionManager.getParams().setConnectionTimeout(connectionTimeout);
		connectionManager.getParams().setDefaultMaxConnectionsPerHost(defaultMaxConnectionsPerHost);
		connectionManager.getParams().setMaxConnectionsPerHost(HostConfiguration.ANY_HOST_CONFIGURATION, maxConnectionsPerHost);
		connectionManager.getParams().setMaxTotalConnections(maxTotalConnections);
		connectionManager.getParams().setSoTimeout(soTimeout);

		httpClient = new HttpClient(connectionManager);

		IdleConnectionTimeoutThread idleConnectionTimeoutThread = new IdleConnectionTimeoutThread();
		idleConnectionTimeoutThread.setName("IdleConnectionTimeoutThread");
		idleConnectionTimeoutThread.setConnectionTimeout(idleConnectionTimeout);
		idleConnectionTimeoutThread.setTimeoutInterval(idleConnectionTimeoutInterval);
		idleConnectionTimeoutThread.setDaemon(true);
		idleConnectionTimeoutThread.addConnectionManager(connectionManager);
		idleConnectionTimeoutThread.start();
	}

	public void invokeRequest(Invocation<?> invocation) throws WsfClientException {
		if (httpClient == null) {
			initializeDefaultHttpClient();
		}
		String endpoint = invocation.getEndpoint();
		String action = invocation.getAction();
		String version = invocation.getVersion();

		Map<String, String> parameters = invocation.getParameters();

		PostMethod method = null;
		try {

			parameters.put(PARAM_VERSION, version);
			parameters.put(PARAM_ACTION, action);

			method = new PostMethod(endpoint);
			for (Entry<String, String> entry : parameters.entrySet()) {
				method.addParameter(entry.getKey(), entry.getValue());
			}

			String logEntry = createLogEntry(parameters);
			logger.debug("Requesting URL {}?{}", endpoint, logEntry);

			int status = httpClient.executeMethod(method);

			if (status == 200) {
				String response = IOUtils.toString(method.getResponseBodyAsStream(), DEFAULT_ENCODING);
				invocation.getResultTransformer().transform(response);
			} else {
				throw new HttpProtocolException(status, method.getStatusText());
			}
		} catch (SocketTimeoutException e) {
			throw new WsfClientException("Socket timeout occured.", e);
		} catch (IOException e) {
			logger.error("I/O error", e);
			throw new WsfClientException("An I/O error occured.", e);
		} catch (ResultTransformationException e) {
			throw new WsfClientException("Could not transform JSON response.", e);
		} finally {
			if (method != null) {
				method.releaseConnection();
			}
		}
	}

	private String createLogEntry(Map<String, String> parameters) throws UnsupportedEncodingException {
		StringBuilder buff = new StringBuilder();
		for (Entry<String, String> it : parameters.entrySet()) {
			String key = it.getKey();
			String value = it.getValue();

			buff.append(key);
			buff.append("=");
			if (value.length() < 1024) {
				buff.append(value);
			} else {
				buff.append(value.substring(0, 1024) + "...");
			}
			buff.append("&");
		}

		if (buff.length() > 0) {
			buff.setLength(buff.length() - 1);
		}
		return buff.toString();
	}

	public void setHttpClient(HttpClient httpClient) {
		this.httpClient = httpClient;
	}

	public void setConnectionTimeout(int connectionTimeout) {
		this.connectionTimeout = connectionTimeout;
	}

	public void setDefaultMaxConnectionsPerHost(int defaultMaxConnectionsPerHost) {
		this.defaultMaxConnectionsPerHost = defaultMaxConnectionsPerHost;
	}

	public void setIdleConnectionTimeout(int idleConnectionTimeout) {
		this.idleConnectionTimeout = idleConnectionTimeout;
	}

	public void setIdleConnectionTimeoutInterval(int idleConnectionTimeoutInterval) {
		this.idleConnectionTimeoutInterval = idleConnectionTimeoutInterval;
	}

	public void setMaxConnectionsPerHost(int maxConnectionsPerHost) {
		this.maxConnectionsPerHost = maxConnectionsPerHost;
	}

	public void setMaxTotalConnections(int maxTotalConnections) {
		this.maxTotalConnections = maxTotalConnections;
	}

	public void setSoTimeout(int soTimeout) {
		this.soTimeout = soTimeout;
	}

}
